#include "main.h"
#include "jis.h"

/**
 * In-memory CSV table
 */
text_t *texts = NULL;
int numcol = 0, numrow = 0, curcol = 0, currow = 0, allocrow = 0;

/**
 * Add a new row with id
 */
int texts_addrow(int id)
{
    if(numrow + 1 >= allocrow) {
        allocrow += 1024;
        engine_ram += 1024 * sizeof(text_t);
        texts = (text_t*)realloc(texts, allocrow * sizeof(text_t));
        if(!texts) { numrow = allocrow = 0; engine_features |= EF_NOMEM; debug("unable to allocate memory for new row"); return 0; }
    }
    memset(&texts[numrow], 0, sizeof(text_t));
    texts[numrow].id = numrow && id ? id : numrow;
    numrow++;
    if(numrow > 1) engine_features |= EF_TEXTS;
    return numrow - 1;
}

/**
 * Look up column by name
 */
int texts_getcol(char *str)
{
    int i, l;

    if(!texts || !numrow || numcol >= 128 || !str || !*str) return 0;
    for(l = 0; str[l] && str[l] != '\"'; l++);
    for(i = 0; i < numcol && memcmp(texts[0].value[i], str, l); i++);
    if(i == numcol) {
        if((texts[0].value[numcol] = (char*)malloc(l + 1))) {
            memcpy(texts[0].value[numcol], str, l);
            texts[0].value[numcol++][l] = 0;
        } else {
            engine_features |= EF_NOMEM;
            debug("unable to allocate memory for language code (%u bytes)", l);
        }
    }
    return i;
}

/**
 * Convert CSV string into in-memory table
 * Returns sum of all strings lengths in table
 */
int csv2mem(char *csv, int len)
{
    char *cend = csv + len, *cstart, term, *s, *d;
    int id, col, row, f = 1;

    /* collect texts into a table */
    texts_free();
    if(!csv) return 0;
    len = 0;
    while(csv < cend) {
        while(csv < cend && (*csv == '\r' || *csv == '\n')) csv++;
        if(csv >= cend || !*csv) break;
        if(*csv == '\"') csv++;
        id = atoi(csv);
        if(!f && !id) break;
        while(csv < cend && *csv && *csv != ';' && *csv != ',' && *csv != '\t') csv++;
        row = texts_addrow(id);
        for(col = 0; *csv == ';' || *csv == ',' || *csv == '\t'; col++) {
            csv++;
            if(*csv != ';' && *csv != ',' && *csv != '\t' && *csv != '\r' && *csv != '\n') {
                if(csv[0] == '\"' && csv[1] == '\"')
                    csv += 2;
                else {
                    if(*csv == '\"') term = *csv++; else term = 0;
                    for(cstart = csv; csv < cend && *csv && *csv != '\r' && *csv != '\n'; csv++) {
                        if(csv[0] == '\"' && csv[1] == '\"') { csv++; continue; }
                        if(*csv == term || (!term && (*csv == ';' || *csv == ',' || *csv == '\t'))) break;
                    }
                    if((texts[row].value[col] = (char*)malloc((uintptr_t)csv - (uintptr_t)cstart + 1))) {
                        for(s = cstart, d = texts[row].value[col]; s < csv; s++, d++, len++) {
                            if(s[0] == '\"' && s[1] == '\"') { s++; len++; }
                            *d = *s;
                        }
                        *d = 0;
                    } else {
                        engine_features |= EF_NOMEM;
                        debug("unable to allocate memory for unquoted csv string (%lu bytes)", (uintptr_t)csv - (uintptr_t)cstart);
                    }
                    if(term == '\"' && *csv == '\"') csv++;
                }
            }
        }
        if(f && !numcol) { numcol = col; f = 0; }
    }
    debug("importing CSV %u columns, %u rows", numcol, numrow);
    return len;
}

/**
 * Convert in-memory table into a CSV string
 */
char *mem2csv(int *size)
{
    int row, col, len, l;
    char *csv, *str, *src;

    if(numrow < 2 || !numcol || !size) return NULL;
    len = 6;
    for(row = 0; row < numrow; row++) {
        len += 3 + 12;
        for(col = 0; col < numcol; col++)
            for(len += 3, src = texts[row].value[col]; src && *src; src++) { len++; if(*src == '\"') len++; }
    }
    if(!(csv = str = (char*)malloc(len))) {
        engine_features |= EF_NOMEM;
        debug("unable to allocate memory for csv (%u bytes)", len);
        return NULL;
    }
    /* first line header */
    str += sprintf(str, "id");
    for(col = 0; col < numcol; col++) {
        *str++ = ';'; l = strlen(texts[0].value[col]);
        memcpy(str, texts[0].value[col], l); str += l;
    }
    str += sprintf(str, "\r\n");
    /* write table into csv format */
    for(row = 1; row < numrow; row++) {
        str += sprintf(str, "%d", texts[row].id);
        for(col = 0; col < numcol; col++) {
            str += sprintf(str, ";\"");
            for(src = texts[row].value[col]; src && *src; src++) {
                if(*src == '\"') *str++ = '\"';
                *str++ = *src;
            }
            *str++ = '\"';
        }
        str += sprintf(str, "\r\n");
    }
    *str = 0;
    *size = (int)((uintptr_t)str - (uintptr_t)csv);
    debug("exporting to CSV %lu bytes", *size);
    return csv;
}

/**
 * Update in-memory table with a PO string
 */
int po2mem(char *po, int len)
{
    char *s, *e, *end = po + len, *str;
    int row = 0;

    if(!po) return 0;
    curcol = 0;
    for(s = po; s < end && *s && row < numrow;) {
        for(; s < end && *s && (*s == ' ' || *s == '\r' || *s == '\n'); s++);
        for(e = s; e < end && *e && *e != '\n'; e++);
        if(!memcmp(s, "msgstr ", 7)) {
            s += 7; str = newstr(&s);
            if(!row) {
                curcol = texts_getcol(str);
                free(str);
            } else {
                if(texts[row].value[curcol]) free(texts[row].value[curcol]);
                texts[row].value[curcol] = str;
            }
            row++;
        }
        s = e;
    }
    debug("importing PO for column %u '%s', %u rows updated", curcol, texts[0].value[curcol] ? texts[0].value[curcol] : "?", row);
    return 1;
}

/**
 * Convert in-memory table into a PO string
 */
char *mem2po(int *size, int col)
{
    int row, len;
    char *po, *str, *src;

    if(numrow < 2 || !numcol || col < 0 || col >= numcol || !size) return NULL;
    len = 128 + strlen(engine_game) + strlen(texts[0].value[0]) + strlen(texts[0].value[col]);
    for(row = 0; row < numrow; row++) {
        for(len += 32 + 12, src = texts[row].value[0]; src && *src; src++, len++)
            if(*src == '\"' || *src == '\'') len++;
        for(src = texts[row].value[col]; src && *src; src++, len++)
            if(*src == '\"' || *src == '\'') len++;
    }
    if(!(po = str = (char*)malloc(len))) {
        engine_features |= EF_NOMEM;
        debug("unable to allocate memory for po (%u bytes)", len);
        return NULL;
    }
    str += sprintf(str, "# GAME RIPPER translation file\r\n# %s\r\n\r\nmsgid \"%s\"\r\nmsgstr \"%s\"\r\n\r\n",
        engine_game, texts[0].value[0], texts[0].value[col]);
    for(row = 1; row < numrow; row++) {
        str += sprintf(str, "# %d\r\nmsgid \"", texts[row].id);
        for(src = texts[row].value[0]; src && *src; src++) {
            if(*src == '\"' || *src == '\'') *str++ = '\\';
            *str++ = *src;
        }
        str += sprintf(str, "\"\r\nmsgstr \"");
        for(src = texts[row].value[col]; src && *src; src++) {
            if(*src == '\"' || *src == '\'') *str++ = '\\';
            *str++ = *src;
        }
        str += sprintf(str, "\"\r\n\r\n");
    }
    *str = 0;
    *size = (int)((uintptr_t)str - (uintptr_t)po);
    debug("exporting to PO column %u '%s', %lu bytes", col, texts[0].value[col] ? texts[0].value[col] : "?", *size);
    return po;
}

/**
 * BASE64 decoder
 */
int base64_decode(char *s, uint8_t *out, int len)
{
    uint8_t *ret = out;
    int b = 0, c = 0, d;
    while(*s && *s != '=' && *s != '\"' && (int)(out - ret) < len) {
        while(*s && (*s == ' ' || *s == '\r' || *s == '\n')) s++;
        if(!*s) break;
        if(*s >= 'A' && *s <= 'Z') d = *s - 'A'; else
        if(*s >= 'a' && *s <= 'z') d = *s - 'a' + 26; else
        if(*s >= '0' && *s <= '9') d = *s - '0' + 52; else
        if(*s == '+' || *s == '-' || *s == '.') d = 62; else
        if(*s == '/' || *s == '_') d = 63; else break;
        b += d; c++; s++;
        if(c == 4) {
            *out++ = (b >> 16);
            *out++ = ((b >> 8) & 0xff);
            *out++ = (b & 0xff);
            b = c = 0;
        } else {
            b <<= 6;
        }
    }
    switch(c) {
        case 1: break;
        case 2: *out++ = (b >> 10); break;
        case 3: *out++ = (b >> 16); *out++ = ((b >> 8) & 0xff); break;
    }
    *out = 0;
    return (int)(out - ret);
}

/**
 * Get a hexadecimal number from a string (num digits)
 */
uint64_t gethex(char *str, int num)
{
    uint64_t ret = 0;
    int i;
    if(!str || !*str || num < 1) return 0;
    if(num > 16) num = 16;
    for(i = 0; i < num; i++, str++) {
        ret <<= 4;
        ret |= (*str >= '0' && *str <= '9' ? *str - '0' : (*str >= 'a' && *str <= 'f' ? *str - 'a' + 10 :
            (*str >= 'A' && *str <= 'F' ? *str - 'A' + 10 : 0)));
    }
    return ret;
}

/**
 * Get a boolean or numeric value from JSON string and return position after the value
 */
char *getvalue(char *json, int *value)
{
    for(; *json && (*json == ':' || *json == ' ' || *json == '\t' || *json == '\r' || *json == '\n'); json++);
    *value = *json == 't' ? 1 : atoi(json);
    while(*json && *json != ',' && *json != ']' && *json != '}' && *json != ' ' && *json != '\t' && *json != '\r' && *json != '\n') json++;
    while(*json && (*json == ',' || *json == ' ' || *json == '\t' || *json == '\r' || *json == '\n')) json++;
    return json - 1;
}

/**
 * Get a quoted string value from JSON string and return position after the value
 */
char *getstr(char *json, char *str, int maxlen)
{
    int i;
    for(; *json && (*json == ':' || *json == ' ' || *json == '\t' || *json == '\r' || *json == '\n'); json++);
    if(*json == '\"') {
        for(i = 0, json++; *json && *json != '\"'; json++) {
            if(json[0] == '\\' && (json[1] == '\"' || json[1] == '\'')) json++;
            if(i < maxlen - 1) str[i++] = *json;
        }
        str[i] = 0;
    } else *str = 0;
    while(*json && *json != ',' && *json != ']' && *json != '}' && *json != ' ' && *json != '\t' && *json != '\r' && *json != '\n') json++;
    while(*json && (*json == ',' || *json == ' ' || *json == '\t' || *json == '\r' || *json == '\n')) json++;
    return json - 1;
}

/**
 * Same as getstr(), but allocated the buffer
 */
char *newstr(char **jsonptr)
{
    char *ret = NULL, *json = *jsonptr;
    int maxlen = 0;

    for(; *json && (*json == ':' || *json == ' ' || *json == '\t' || *json == '\r' || *json == '\n'); json++);
    if(*json == '\"') {
        for(maxlen = 1, json++; *json && *json != '\"'; json++, maxlen++)
            if(json[0] == '\\') { json++; maxlen++; }
        if((ret = (char*)malloc(maxlen))) {
            *jsonptr = getstr(*jsonptr, ret, maxlen);
        } else {
            engine_features |= EF_NOMEM;
            debug("unable to allocate memory for unquoted string (%u bytes)", maxlen);
            *jsonptr = json + 1;
        }
    }
    return ret;
}

/**
 * Convert u UNICODE codepoint into UTF-8 multibyte
 */
char *toutf8(char *str, uint32_t u)
{
    if(u < 0x80) *str++ = u; else
    if(u < 0x800) { *str++ = ((u>>6)&0x1F)|0xC0; *str++ = (u&0x3F)|0x80; }
    else { *str++ = ((u>>12)&0x0F)|0xE0; *str++ = ((u>>6)&0x3F)|0x80; *str++ = (u&0x3F)|0x80; }
    *str = 0;
    return str;
}

/**
 * Checks if a string is a valid zero terminated UTF-8
 */
int validutf8(char *str)
{
    uint8_t *s = (uint8_t*)str;
    while(*s) {
        if(!(s[0] & 0x80)) s++; else
        if((s[0] & 0xE0) == 0xC0 && (s[1] & 0xC0) == 0x80) s += 2; else
        if((s[0] & 0xF0) == 0xE0 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) s += 3; else
        if((s[0] & 0xF8) == 0xF0 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80 && (s[3] & 0xC0) == 0x80) s += 4;
        else return 0;
    }
    return 1;
}

/**
 * Free text table
 */
void texts_free(void)
{
    int i, j;

    if(texts) {
        for(i = 0; i < numrow; i++) {
            for(j = 0; j < numcol; j++)
                if(texts[i].value[j]) free(texts[i].value[j]);
        }
        free(texts); texts = NULL;
    }
    numcol = numrow = curcol = currow = allocrow = 0;
}

/**
 * UI Controller
 */
void texts_ctrl(void)
{
    char *buf, tmp[256];
    int size;
    uint64_t len;

    switch(event.type) {
        case SDL_KEYDOWN:
            switch(event.key.keysym.sym) {
                case SDLK_LEFT: curcol--; break;
                case SDLK_RIGHT: curcol++; break;
                case SDLK_UP: currow--; break;
                case SDLK_DOWN: currow++; break;
                case SDLK_HOME: currow = 0; break;
                case SDLK_END: currow = numrow - 2; break;
                case SDLK_PAGEUP: currow -= ((screen_h - 42) / 18); break;
                case SDLK_PAGEDOWN: currow += ((screen_h - 42) / 18); break;
                case SDLK_RETURN: field = pressedfield = 0; break;
                case SDLK_SPACE: engine_backup ^= 1; break;
            }
        break;
        case SDL_KEYUP:
            switch(event.key.keysym.sym) {
                case SDLK_RETURN: field = pressedfield = 0; goto doexp;
            }
        break;
        case SDL_MOUSEBUTTONUP:
doexp:      if(field == 0 && pressedfield == 0 && curcol >= 0 && curcol < numcol) {
                buf = mem2csv(&size);
                savedialog(NULL, "texts.csv", (uint8_t*)buf, (uint64_t)size);
            } else
            if(field == 1 && pressedfield == 1 && curcol >= 0 && curcol < numcol) {
                buf = mem2po(&size, curcol);
                strcpy(tmp, texts[0].value[curcol]);
                strcat(tmp, ".po");
                savedialog(NULL, tmp, (uint8_t*)buf, (uint64_t)size);
            } else
            if(field == 2 && pressedfield == 2) {
                opendialog(LOADCSV, 0);
                if(fn[0] && (buf = (char*)getfile(fn, &len))) {
                    if(!memcmp(buf, "id", 2) || !memcmp(buf, "\"id", 3)) {
                        if(csv2mem(buf, len)) engine_translate();
                    } else
                    if(strstr(buf, "msgstr")) {
                        if(po2mem(buf, len)) engine_translate();
                    } else
                        msgbox(1, UNKNOWN, fn);
                    free(buf);
                }
            }
            if(field == 3 && pressedfield == 3) curcol--; else
            if(field == 4 && pressedfield == 4) curcol++;
        break;
        case SDL_MOUSEBUTTONDOWN:
            if(field == 5) engine_backup ^= 1; else
            if(my < 38 + screen_h - 21) {
                currow = (scrcur + my - 38) / 18;
            }
        break;
    }
    if(curcol >= numcol) curcol = 0;
    if(curcol < 0) curcol = numcol - 1;
    if(currow >= numrow - 1) currow = numrow - 2;
    if(currow < 0) currow = 0;
    if(scrcur > currow * 18) scrcur = currow * 18;
    if(scrcur < currow * 18 - (screen_h - 44)) scrcur = currow * 18 - (screen_h - 44);
}

/**
 * UI View
 */
void texts_view(void)
{
    char tmp[256];
    int i, j, tot = numrow * 18 - (screen_h - 21 - 4);

    /* failsafes */
    if(!numcol || numrow < 2 || !texts) {
        engine_features &= ~EF_TEXTS;
        tab = TAB_INFO;
        refresh = 1;
        return;
    }
    if(curcol >= numcol) curcol = numcol - 1;
    if(curcol < 0) curcol = 0;
    if(currow >= numrow - 1) currow = numrow - 2;
    if(currow < 0) currow = 0;

    strcpy(tmp, lang[EXPORT]); strcat(tmp, " (CSV)");
    i = textlen(tmp) + 16;
    j = textlen(lang[IMPORT]) + 16;
    if(i < j) i = j;
    fields[0].x = 2; fields[0].y = screen_h - 23; fields[0].w = i; fields[0].h = 21;
    button(fields[0].x, fields[0].y, i, 21, pressedfield == 0, tmp);

    strcpy(tmp, lang[EXPORT]); strcat(tmp, " (PO)");
    fields[1].x = i + 6; fields[1].y = screen_h - 23; fields[1].w = i; fields[1].h = 21;
    button(fields[1].x, fields[1].y, i, 21, pressedfield == 1, tmp);

    fields[2].x = 2 * i + 10; fields[2].y = screen_h - 23; fields[2].w = i; fields[2].h = 21;
    button(fields[2].x, fields[2].y, i, 21, pressedfield == 2, lang[IMPORT]);
    i = 3 * i + 32;

    fields[3].x = i; fields[3].y = screen_h - 23; fields[3].w = fields[3].h = 21;
    button(fields[3].x, fields[3].y, 21, 21, pressedfield == 3, "⏴");
    i += 21;
    txtfld(i, screen_h - 23, 48, 21, texts[0].value[curcol]);
    i += 48; fields[4].x = i; fields[4].y = screen_h - 23; fields[4].w = fields[4].h = 21;
    button(fields[4].x, fields[4].y, 21, 21, pressedfield == 4, "⏵");
    i += 4 + 21 + 16;

    fields[5].x = i; fields[5].y = screen_h - 22; fields[5].w = screen_w - 10 - i; fields[5].h = 16;
    chkbox(fields[5].x, fields[5].y, engine_backup, lang[BACKUP]);

    hscroll(screen_w - 10, 0, screen_h, scrcur, tot, pressedfield == 999);

    crop_h = screen_h - 21 - 4;
    for(i = scrcur / 18; i < numrow - 1 && i * 18 - scrcur < crop_h; i++) {
        crop_w = 80;
        box(16, 3 + i * 18 - scrcur, 64, 16, i == currow ? FG : BG - 0x080808);
        sprintf(tmp, "%8d", texts[i + 1].id);
        text(16, 3 + i * 18 - scrcur, i == currow ? BG : FG, tmp);
        crop_w = screen_w - 16;
        box(82, 3 + i * 18 - scrcur, screen_w - 98, 16, i == currow ? FG : BG - 0x080808);
        text(82, 3 + i * 18 - scrcur, i == currow ? BG : FG, texts[i + 1].value[curcol]);
    }

}

